﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace Dixon.Jscp
{
    /// <summary>
    ///     An interface to be implemented by object that can be
    ///     written out as JavaScript.
    /// </summary>
    public interface IScriptable
    {

        /// <summary>
        /// Writes this object as JavaScript using the
        /// script writer provided
        /// </summary>
        /// <param name="writer">the destination writer</param>
        void WriteScript(ScriptWriter writer);
    }


    /// <summary>
    ///     <para>
    ///         A lightweight class that makes it easy to programmatically
    ///         create JavaScript output
    ///     </para>
    ///     <para>
    ///         Every output method returns the current script writer instance,
    ///         which makes it easy to chain multiple calls together.
    ///     </para>    
    /// </summary>
    public class ScriptWriter
    {
        /// <summary>
        /// The TextWriter that writes to the destination
        /// stream
        /// </summary>
        private TextWriter writer;

        /// <summary>
        /// Creates a new script writer
        /// </summary>
        /// <param name="outputWriter">
        /// A TextWriter instance that writes to the desired
        /// output destination.
        /// </param>
        public ScriptWriter(TextWriter outputWriter)
        {
            writer = outputWriter;
        }

        /// <summary>
        /// Opens a JavaScript array by outputting a '['
        /// </summary>
        /// <returns>this script writer instance</returns>
        public ScriptWriter StartArray()
        {
            writer.Write("[");
            return this;
        }


        /// <summary>
        /// Closes a JavaScript array by outputting a ']' character
        /// </summary>
        /// <returns>this script writer instance</returns>
        public ScriptWriter EndArray()
        {
            writer.Write("]");
            return this;
        }

        /// <summary>
        /// Starts an anonymous JavaScript object, which can be
        /// used to represent associative data structures (key-value
        /// pairs)
        /// </summary>
        /// <returns>this script writer instance</returns>
        public ScriptWriter StartObject()
        {
            writer.Write("{");
            return this;
        }

        /// <summary>
        /// Closes an anonymous JavaScript object by outputting a '}'
        /// character
        /// </summary>
        /// <returns>this script writer instance</returns>
        public ScriptWriter EndObject()
        {
            writer.Write("}");
            return this;
        }

        /// <summary>
        /// Writes a string of text as a JavaScript string, escaping
        /// any special characters
        /// </summary>
        /// <param name="text">an arbitrary string</param>
        /// <returns>this script writer instance</returns>
        public ScriptWriter WriteText(string text)
        {
            writer.Write(ScriptWriter.Escape(text));
            return this;
        }

        /// <summary>
        /// Writes a delimiter that separates items in arrays
        /// and anonymous objects
        /// </summary>
        /// <returns>this script writer instance</returns>
        public ScriptWriter EndItem()
        {
            writer.Write(",");
            return this;
        }


        /// <summary>
        /// Writes a key/value pair in the context of a JavaScript object 
        /// (e.g. 'foo' : 'bar'). The ending comma is NOT output by this 
        /// method
        /// </summary>
        /// <param name="key">the property name</param>
        /// <param name="value">the value of the property</param>
        /// <returns>this script writer instance</returns>
        public ScriptWriter WriteKeyValuePair(string key, string value)
        {
            writer.Write(ScriptWriter.Escape(key) + ":" + ScriptWriter.Escape(value));
            return this;
        }


        /// <summary>
        /// Outputs a property name followed by a colon. It's up to the user to
        /// follow up with a value, which may be a string, an object an array or 
        /// anything else supported by JavaScript
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public ScriptWriter WriteKey(string key)
        {
            writer.Write(ScriptWriter.Escape(key) + ":");
            return this;
        }

        /// <summary>
        /// Outputs a scriptable item, which implements the
        /// IScriptable interface, to the output of this script
        /// writer.
        /// </summary>
        /// <param name="item">an IScriptable item</param>
        /// <returns>this script writer instance</returns>
        public ScriptWriter Write(IScriptable item)
        {
            item.WriteScript(this);
            return this;
        }


        /// <summary>
        /// Writes multiple IScriptable items as a JavaScript array
        /// </summary>
        /// <remarks>
        /// While the IScriptable interface does not impose any constraints
        /// on the output, all instances of this interface should output a single
        /// instance of a legitimate JavaScript type
        /// </remarks>
        /// <param name="items">a collection of scriptable items</param>
        /// <returns>this script writer instance</returns>
        public ScriptWriter WriteItems(IEnumerable<IScriptable> items)
        {            
            this.StartArray();

            using (var enumerator = items.GetEnumerator())
            {
                if (enumerator.MoveNext())
                {
                    Write(enumerator.Current);
                    while(enumerator.MoveNext())
                    {
                        writer.Write(",");
                        Write(enumerator.Current);
                    }
                }
            }

            return this.EndArray();
        }

        /// <summary>
        /// Outputs the provided content directly to the underlying 
        /// stream without processing or escaping. The content should 
        /// represent valid JavaScript code that is applicable
        /// to the current context.
        /// </summary>
        /// <param name="content">raw JavaScript code</param>
        /// <returns>this script writer instance</returns>
        public ScriptWriter WriteRaw(object content)
        {
            writer.Write(content);
            return this;
        }

        /// <summary>
        /// Starts a JS function or method call (e.g. "foo(")
        /// </summary>
        /// <param name="method">the name of the method to call</param>
        /// <returns>this script writer instance</returns>
        public ScriptWriter StartCall(string method)
        {
            writer.Write(method + "(");
            return this;
        }

        /// <summary>
        /// Outputs the ending parenthesis for a method call 
        /// previously opened with StartCall()
        /// </summary>
        /// <returns>this script writer instance</returns>
        public ScriptWriter EndCall()
        {
            writer.Write(")");
            return this;
        }


        /// <summary>
        /// Converts a .NET string to a JavaScript string by escaping
        /// special characters
        /// </summary>
        /// <param name="source">any string</param>
        /// <returns>a quoted, escaped JavaScript string</returns>
        public static string Escape(string source)
        {
            return (source == null) ? "null" : "'" + source
                .Replace(@"\", @"\\")
                .Replace(@"\n", "' + \"\\n\" + '")
                .Replace("'", @"\'") + "'";
        }
    }
}
